Object Oriented Programming in Python

Manish Patel

Jan 11, 2024

CONCEPT OF OOP

Everything in Python is a Class / Object

x = 42
print(type(x))

y = 'hello'
print(type(y))

def z():
    pass
print(type(z))

import math
print(type(math))

print(type(type))
<class 'int'>
<class 'str'>
<class 'function'>
<class 'module'>
<class 'type'>

PYTHON OOPS CONCEPTS

TERMINOLOGY

Object-oriented programming

A paradigm in which data and operations are modeled as intimately paired, rather than as separate elements.

Class

A group of objects that share commonality in their underlying encoding and supported behaviors.s are modeled as intimately paired, rather than as separate elements.

Object

An object has state, behavior, and identity; the structure and behavior of similar objects are defined in their common class. Some things are not objects, but are attributes; e.g. age, color. Attributes may be properties of some object.

Instance

A single object drawn from a given class.

Attribute

One of the pieces of data used in representing the state of an object. Equivalently termed a data member, field, or instance variable

Method

A formal operation supported by all objects of a given class.

OOPS STAGES

  1. The first stage is the class definition. Like function definitions, this stage is where you write the blueprint to be used when called.
  2. The second stage is called instantiation. It is the process of creating an object from the class definition.
  3. After an object is instantiated, it is known as an instance. You may have multiple instances from a single class definition.

Class

A blueprint to make objects

# Example 1: Creating Class and Object in Python

class Person:
    "This is a person class"
    age = 10

    def greet(self):
        print('Hello')

print(Person.age)      # Output: 10

print(Person.greet)    # Output: <function Person.greet>

print(Person.__doc__)  # Output: 'This is my second class'
10
<function Person.greet at 0x000001F9695FE0E0>
This is a person class

Object

An instance of a Class

class Person:
    pass

x = Person()
y = Person()
x,y
(<__main__.Person at 0x189adcf58e0>, <__main__.Person at 0x189adfa5eb0>)

OBJECTS

class Python:
    count = 0
    def __str__(self):
        Python.count+=1
        return f'this is a {Python.count} object'
PP = Python()
dd = Python()
mm = Python()

print(PP)
print(dd)
print(mm)
this is a 1 object
this is a 2 object
this is a 3 object

CONSTRUCTOR METHOD

Constructors are methods that are useful for any kind of initialization you want to perform with the objects. Note:
A constructor is executed as soon as an object of a class is instantiated

class MyNum(object):
    def __init__(self):
        print("Calling the __init__() constructor!\n")
        self.val = 0
    def increment(self):
        self.val = self.val + 1
        print(self.val)
dd = MyNum()
dd.increment()  # will print 1
dd.increment()  # will print 2
Calling the __init__() constructor!

1
2

WORKER OBJECT

class Employee: # Class definition                                                       
    'Common base class for all employees' # Docstring                               
    empCount = 0 # Class variable
    def __init__(self, name, salary): # constructor method
            self. name = name         # instance variable
            self. salary = salary     # instance variable  
            Employee. empCount += 1   
    def displayCount(self):           # instance method
        print ("Total Employee %d" % Employee. empCount)
    
    def displayEmployee(self):
        print ("Name : ", self. name,  ", Salary: " , self. salary)
    def __str__(self): #special method
        return f'The name is {self.name} and salary is {self.salary}'
    
class Worker(Employee):
        pass

OBJECT ATTRIBUTES

emp1 = Employee("Zara", 2000)     # First object
emp2 = Employee("Manni", 5000)    # Second object
emp1.displayEmployee()           # Method
emp2.displayEmployee()            # Method
print(emp1)
print(emp2)
print(hasattr(emp1, 'salary' )) # Returns true if 'salary' attribute exists
print(getattr(emp1, 'salary' ))  # Returns value of 'salary' attribute
print(setattr(emp1, 'age' , 8)) # Set attribute 'age' at 8
print(emp1.age)
print(delattr(emp1, 'salary' ))  # Delete attribute 'age'
print ("Employee.__doc__:", Employee. __doc__)
print ("Employee.__name__:", Employee. __name__)
print ("Employee.__module__:", Employee. __module__)
print ("Employee.__bases__:", Employee. __bases__)
print ("Employee.__dict__:", Employee. __dict__ )
print(isinstance(emp1,Employee))
print(issubclass(Worker,Employee))
Name :  Zara , Salary:  2000
Name :  Manni , Salary:  5000
The name is Zara and salary is 2000
The name is Manni and salary is 5000
True
2000
None
8
None
Employee.__doc__: Common base class for all employees
Employee.__name__: Employee
Employee.__module__: __main__
Employee.__bases__: (<class 'object'>,)
Employee.__dict__: {'__module__': '__main__', '__doc__': 'Common base class for all employees', 'empCount': 2, '__init__': <function Employee.__init__ at 0x00000189AD9E90D0>, 'displayCount': <function Employee.displayCount at 0x00000189AD9E9280>, 'displayEmployee': <function Employee.displayEmployee at 0x00000189AD9E9310>, '__str__': <function Employee.__str__ at 0x00000189AD9E93A0>, '__dict__': <attribute '__dict__' of 'Employee' objects>, '__weakref__': <attribute '__weakref__' of 'Employee' objects>}
True
True

## What is self in Python?

In object-oriented programming, whenever we define methods for a class, we use self as the first parameter in each case. Let’s look at the definition of a class called Point.the method call p1.distance() is actually equivalent to Point.distance(p1).

class Point(object):
    def __init__(self,x = 0,y = 0):
        self.x = x
        self.y = y
    def distance(self):
        """Find distance from origin"""
        return (self.x**2 + self.y**2) ** 0.5
p1 = Point(6,9)
print(p1.distance())
print(type(Point.distance))
print(type(p1.distance))
10.816653826391969
<class 'function'>
<class 'method'>

Self Can Be Avoided

By now you are clear that the object (instance) itself is passed along as the first argument, automatically. This implicit behavior can be avoided while making a static method. Consider the following simple example:

class A(object):

    @staticmethod
    def stat_meth():
        print("Look no self was passed")

Here, @staticmethod is a function decorator that makes stat_meth() static. Let us instantiate this class and call the method.

a = A()
a.stat_meth()
Look no self was passed
print(type(A.stat_meth))
print(type(a.stat_meth))
<class 'function'>
<class 'function'>

ANOTHER EXAMPLE

class Student:
    'Common base class for all students'
    student_count=0

    def __init__(self, name, id):
        self.name = name
        self.id = id
        Student.student_count+=1

    def printStudentData(self):
        print ("Name : ", self.name, ", Id : ", self.id)
std1=Student("Milan",101)
std2=Student("Vijay",102)
std3=Student("Chirag",103)
print("Total Student : ",Student.student_count)
print ("Student.__doc__:", Student.__doc__)
print ("StudentStudent.__name__:", Student.__name__)
print ("Student.__module__:", Student.__module__)
print ("Student.__bases__:", Student.__bases__)
print ("Student.__dict__:", Student.__dict__)
Total Student :  3
Student.__doc__: Common base class for all students
StudentStudent.__name__: Student
Student.__module__: __main__
Student.__bases__: (<class 'object'>,)
Student.__dict__: {'__module__': '__main__', '__doc__': 'Common base class for all students', 'student_count': 3, '__init__': <function Student.__init__ at 0x000001F9695FE680>, 'printStudentData': <function Student.printStudentData at 0x000001F9695FEA70>, '__dict__': <attribute '__dict__' of 'Student' objects>, '__weakref__': <attribute '__weakref__' of 'Student' objects>}

Data Hiding

class MyClass(object): # Defining class 
    def __init__(self, x, y, z):
        self.var1 = x # public data member
        self._var2 = y # protected data member
        self.__var3 = z # private data member
        
obj = MyClass(3,4,5)
print(obj.var1)
print(obj._var2)
3
4
print(obj.__var3)
AttributeError: 'MyClass' object has no attribute '__var3'
print(obj._MyClass__var3)
print(type(obj))
print(issubclass(MyClass,object))
5
<class '__main__.MyClass'>
True

Class Variable

A variable of class shared by all instances

class Person:
    m = 'hello'
x = Person()
y = Person()


print(x.m)
print(y.m)
print(Person.m)
hello
hello
hello

IMPLEMENTATION

class Person:
    species  = 'Homo sapiens'
    count = 0

    def __init__(self,name,age):
        self.name = name
        self.age = age
        Person.count+=1

    def display(self):
         print(f'{self.name} is {self.age} years old')
         
p1 = Person('John',20)
p2 = Person('Jack',34)

p1.display()
p2.display()

print(Person.count)
p3=Person('Jill', 40)
p4=Person('Jane', 35)
print(Person.count) 
John is 20 years old
Jack is 34 years old
2
4

CLASS VARIABLE - book example

class Book():
    x = 5
    def __init__(self):
        self.x = 100
    def display(self):
        print(self.x)
        print(Book.x)

b = Book()
b.display()

print(Book.x)
print(b.x)
100
5
5
100

Instance Variable

A variable defined inside class which can only be accessed by current instance

class Python:
    pass
x = Python()
y = Python()
x.m = 60
y.n = 70
x.m
60
y.n
70
print(Python.__module__)
print(x.__class__)
__main__
<class '__main__.Python'>

CLASS VS INSTANCE ATTRIBUTES

class YourClass(object):
    classy = "class value"


dd = YourClass()
print(dd.classy)  # < This should return the string "class value"

dd.classy = "Instance value"
print(dd.classy)  # This should return the string "Instance value"

# This will delete the value set for 'dd.classy' in the instance.
del dd.classy
# Since the overriding attribute was deleted, this will print 'class value'.
print(dd.classy)
class value
Instance value
class value

ABSTRACTION IN PYTHON

Program to demonstrate the difference between Abstraction & Encapsulation

# The internal representation of an object of foo class ➀–➅ is hidden outside the class → Encapsulation. 
class foo:
    def __init__(self, a, b):
        self.a = a
        self.b = b
    def add(self):
        return self.a * self.b
    
# Any accessible member (data/method) of an object of foo is restricted and can only be accessed by that object 
# Implementation of add() method is hidden → Abstraction.
foo_object = foo(3,4)
foo_object.add()
12

CLASS METHOD AND STATIC METHOD

class MyClass():

    a = 5
    def __init__(self, x):
          self.x = x
    def method1(self):
          print(self.x)
    @classmethod      
    def method2(cls):
          print(cls.a)
    @staticmethod
    def method3(m,n):
          return m+n         
f = MyClass(9)
f.method1()
f.method2()
f.method3(4,6)
9
5
10

DATA HIDING / ENCAPSULATION

CAPSULE

CAR EXAMPLE

image.png

Variables meaning

Encapsulation - variables

image.png

DATA HIDING EXAMPLE

# Data Hiding

class MyClass(object):             # Defining class 
    def __init__(self, x, y, z):   # Constructor method
        self.var1 = x              # public data member
        self._var2 = y             # protected data member
        self.__var3 = z            # private data member
        
obj = MyClass(3,4,5)
print(obj.var1)
print(obj._var2)

# print(obj.__var3)

print(obj._MyClass__var3)
3
4
5

OOPS CONCEPT IN PYTHON - SUMMARY

CLASSES

class Website:
    pass

OBJECT

github = Website()
github.url = 'https://github.com/'
github.founding_year = 2008
github.free_to_use = True

INSTANCE/OBJECT VARIABLES

print(github.founding_year)
print(github.free_to_use)
print(github.url)
2008
True
https://github.com/

NEW OBJECT

newob = Website()
newob.url
AttributeError: 'Website' object has no attribute 'url'

OBJECT DICTIONARY

print(github.__dict__)
print(newob.__dict__)
{'url': 'https://github.com/', 'founding_year': 2008, 'free_to_use': True}
{}

CLASS VARIABLES

Website.isonline = True
print(github.isonline)
print(newob.isonline)
True
True

Functions and Methods

def website_info(website):
    print("URL:", website.url)
    print("Founding year:", website.founding_year)
    print("Free to use:", website.free_to_use)

website_info(github)
URL: https://github.com/
Founding year: 2008
Free to use: True

Class.method(instance) does the same thing as instance.method()

  • 'hello'.lower() is the same as str.lower('hello').
Website.info = website_info
Website.info(github)
URL: https://github.com/
Founding year: 2008
Free to use: True
github.info()
URL: https://github.com/
Founding year: 2008
Free to use: True

Defining Methods When Defining the Class

class Website:
    def info(self):     # self will be github
        print("URL:", self.url)
        print("Founding year:", self.founding_year)
        print("Free to use:", self.free_to_use)

github = Website()
github.url = 'https://github.com/'
github.founding_year = 2008
github.free_to_use = True
github.info()
URL: https://github.com/
Founding year: 2008
Free to use: True

CONSTRUCTOR

class Website:
    def initialize(self, url, founding_year, free_to_use):
        self.url = url
        self.founding_year = founding_year
        self.free_to_use = free_to_use
    def info(self):
        print("URL:", self.url)
        print("Founding year:", self.founding_year)
        print("Free to use:", self.free_to_use)

github = Website()
github.initialize('https://github.com/', 2008, True)
github.info()
URL: https://github.com/
Founding year: 2008
Free to use: True

__init__ constructor

class Website:
    def __init__(self, url, founding_year, free_to_use):
        self.url = url
        self.founding_year = founding_year
        self.free_to_use = free_to_use
    def info(self):
        print("URL:", self.url)
        print("Founding year:", self.founding_year)
        print("Free to use:", self.free_to_use)

github = Website('https://github.com/', 2008, True)
github.info()
URL: https://github.com/
Founding year: 2008
Free to use: True

DESTRUCTOR METHOD

  • In object-oriented programming (OOP) with Python, a destructor is a special method that is automatically called when an object is about to be destroyed or deallocated. It is used to clean up resources or perform any necessary cleanup operations before the object is removed from memory.

  • In Python, the destructor method is named __del__.

EXAMPLE

class MyClass:
    def __init__(self, name):
        self.name = name
        print(f"{self.name} created")

    def __del__(self):
        print(f"{self.name} destroyed")

Creating instances of MyClass

obj1 = MyClass("Object 1")
obj2 = MyClass("Object 2")
Object 1 created
Object 2 created

Deleting instances explicitly

del obj1
del obj2
Object 1 destroyed
Object 2 destroyed
  • Keep in mind that relying on the __del__ method for cleanup is not always recommended. It’s more common to use context managers (with the with statement) or other mechanisms for resource management in Python. The __del__ method is called by the garbage collector, and the exact timing of its execution is not guaranteed.

Generators

The generators offer a comfortable method to generate iterators, and that's why they are called generators.

Method of working:

  • A generator is called like a function. Its return value is an iterator, i.e. a generator object. The code of the generator will not be executed at this stage.
  • The iterator can be used by calling the next method. The first time the execution starts like a function, i.e. the first line of code within the body of the iterator. The code is executed until a yield statement is reached.
  • yield returns the value of the expression, which is following the keyword yield. This is like a function, but Python keeps track of the position of this yield and the state of the local variables is stored for the next call. At the next call, the execution continues with the statement following the yield statement and the variables have the same values as they had in the previous call.
  • The iterator is finished, if the generator body is completely worked through or if the program flow encounters a return statement without a value.

EXAMPLE

def city_generator():
    yield "Hamburg"
    yield "Konstanz"
    yield "Berlin"
    yield "Zurich"
    yield "Schaffhausen"
    yield "Stuttgart" 
    
city = city_generator()
print(next(city))
print(next(city))
print(next(city))
print(next(city))
print(next(city))
print(next(city))
print(next(city))
Hamburg
Konstanz
Berlin
Zurich
Schaffhausen
Stuttgart
StopIteration: 

EXAMPLE

def fibonacci(n):
    """ A generator for creating the Fibonacci numbers """
    a, b, counter = 0, 1, 1
    while True:
        if (counter > n): 
            return
        yield a
        a, b = b, a + b
        counter += 1
f = fibonacci(6)
for x in f:
    print(x, " ", end="") # 
0  1  1  2  3  5